# This file is part of Indico.
# Copyright (C) 2002 - 2025 CERN
#
# Indico is free software; you can redistribute it and/or
# modify it under the terms of the MIT License; see the
# LICENSE file for more details.

from ipaddress import ip_address

from flask import has_request_context, request, session
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext.declarative import declared_attr

from indico.core.db import db
from indico.core.db.sqlalchemy import PyIPNetwork
from indico.core.db.sqlalchemy.principals import PrincipalType
from indico.util.string import format_repr


class IPNetworkGroup(db.Model):
    __tablename__ = 'ip_network_groups'
    principal_type = PrincipalType.network
    principal_order = 1

    @declared_attr
    def __table_args__(cls):
        return (db.Index('ix_uq_ip_network_groups_name_lower', db.func.lower(cls.name), unique=True),
                {'schema': 'indico'})

    id = db.Column(
        db.Integer,
        primary_key=True
    )
    name = db.Column(
        db.String,
        nullable=False
    )
    description = db.Column(
        db.Text,
        nullable=False,
        default=''
    )
    #: Whether the network group is hidden in ACL forms
    hidden = db.Column(
        db.Boolean,
        nullable=False,
        default=False
    )
    #: Grants all IPs in the network group read access to all attachments
    attachment_access_override = db.Column(
        db.Boolean,
        nullable=False,
        default=False
    )

    _networks = db.relationship(
        'IPNetwork',
        lazy=False,
        cascade='all, delete-orphan',
        collection_class=set,
        backref=db.backref(
            'group',
            lazy=True
        )
    )
    networks = association_proxy('_networks', 'network', creator=lambda v: IPNetwork(network=v))

    # relationship backrefs:
    # - in_category_acls (CategoryPrincipal.ip_network_group)
    # - in_event_acls (EventPrincipal.ip_network_group)

    @property
    def identifier(self):
        return f'IPNetworkGroup:{self.id}'

    @property
    def persistent_identifier(self):
        """A persistent version of this object's identifier.

        This is currently identical to the regular identifier, since it does not
        contain any signatures or similar data that could change.
        """
        return self.identifier

    def __repr__(self):
        return format_repr(self, 'id', 'name', hidden=False, attachment_access_override=False)

    def __contains__(self, user):
        # This method is called via ``user in principal`` during ACL checks.
        # We have to take the IP from the request so if there's no request
        # (e.g. in the shell) we never grant IP-based access; same if we check
        # for a different user than the one from the current session.
        if not has_request_context() or not request.remote_addr:
            return False
        if session.user != user:
            return False
        return self.contains_ip(str(request.remote_addr))

    def contains_ip(self, ip):
        ip = ip_address(ip)
        return any(ip in network for network in self.networks)

    @property
    def locator(self):
        return {'network_group_id': self.id}


class IPNetwork(db.Model):
    __tablename__ = 'ip_networks'
    __table_args__ = {'schema': 'indico'}

    group_id = db.Column(
        db.Integer,
        db.ForeignKey('indico.ip_network_groups.id'),
        primary_key=True,
        autoincrement=False
    )
    network = db.Column(
        PyIPNetwork,
        primary_key=True,
        nullable=False
    )

    # relationship backrefs:
    # - group (IPNetworkGroup._networks)

    def __repr__(self):
        return format_repr(self, 'group_id', 'network')
